package com.cx.common.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.cx.common.annotation.DistributedLockAnnotation;
import com.cx.common.constant.RedisConstant;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


/**
 * @author cx
 * @description: 分布式锁切面
 * @date 2023/7/28 9:27
 */
@Aspect
public class DistributedLockAspect {

    public DistributedLockAspect(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    private final RedisTemplate<String, String> redisTemplate;

    @Pointcut("@annotation(com.cx.common.annotation.DistributedLockAnnotation)")
    public void lockPointcut() {
    }

    @Around("lockPointcut() && @annotation(annotation)")
    public Object addDistributedLock(ProceedingJoinPoint point, DistributedLockAnnotation annotation) {
        String key = annotation.key();
        long expirationTime = annotation.expirationTime();
        if (StrUtil.isEmpty(key)) {
            throw new RuntimeException("key must is not empty");
        }
        if (expirationTime > 0 && annotation.lockRetryInterval() < 0) {
            throw new RuntimeException("lockRetryInterval must is greater than or equal 0");
        }
        String value = IdUtil.simpleUUID();
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, expirationTime, TimeUnit.SECONDS);
        if (!Boolean.TRUE.equals(result)) {
            for (int i = 0; i < annotation.lockRetryCount(); i++) {
                if (annotation.lockRetryInterval() > 0) {
                    try {
                        Thread.sleep(annotation.lockRetryInterval());
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                result = redisTemplate.opsForValue().setIfAbsent(key, value, expirationTime, TimeUnit.SECONDS);
                if (Boolean.TRUE.equals(result)) {
                    break;
                }
            }
            if (!Boolean.TRUE.equals(result)) {
                return null;
            }
        }
        try {
            return point.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        } finally {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return -1 end";
            RedisScript<Object> redisScript = new DefaultRedisScript<>(script, Object.class);
            List<?> returnResult = (List<?>) redisTemplate.execute(redisScript, Collections.singletonList(key), value);
            if (CollUtil.isEmpty(returnResult) || Objects.equals(String.valueOf(returnResult.get(0)), RedisConstant.CUSTOM_FAIL_CODE)) {
                for (int i = 0; i < annotation.unlockRetryCount(); i++) {
                    returnResult = (List<?>) redisTemplate.execute(redisScript, Collections.singletonList(key), value);
                    if (CollUtil.isNotEmpty(returnResult) && Objects.equals(String.valueOf(returnResult.get(0)), RedisConstant.CUSTOM_SUCCESS_CODE)) {
                        break;
                    }
                }
            }
        }
    }
}
